module Charter

using StaticArrays

include("helpers.jl")
using .Helpers

include("model.jl")
import .Model
const XY = Model.XY
const Bucket = Model.Bucket
const PriceType = Model.PriceType

include("interaction.jl")
import .Interaction

include("bitmex/core.jl")
import .Bitmex

include("transform.jl")
import .Transform
const Tr = Transform

include("tech.jl")
import .Tech

using Colors
using Statistics

import Cairo
import Gtk
import GtkReactive
import Reactive

const C = Cairo
const G = Gtk
const GR = GtkReactive
const R = Reactive

using Lazy

function gtk_to_signal(widget,
                       event_type,
                       proc_f,
                       init :: T) where {T}
  signal = R.Signal(init)
  G.signal_connect(widget, event_type) do _, event
    @>> proc_f(event) push!(signal)
  end
  signal
end

struct Candle
  tail_top :: PriceType
  body_top :: PriceType
  body_bottom :: PriceType
  tail_bottom :: PriceType
  body_top_last :: Bool
end

function bucket2candle(b :: Bucket)
  o = b.open
  h = b.high
  l = b.low
  c = b.close
  body_top = o > c ? o : c
  body_bottom = o < c ? o : c
  body_top_last = o < c
  tail_top = h
  tail_bottom = l
  Candle(tail_top, body_top, body_bottom, tail_bottom, body_top_last)
end

#=
function load_buckets() :: Vector{Bucket}
  json = JSON.parsefile("../beth.json")
  map(json) do dict
    Bucket(dict["open"], dict["high"], dict["low"], dict["close"])
  end
end
=#

function draw_candle(ctx, x, candle)
  if candle.body_top_last
    color = colorant"white"
  else
    color = colorant"red"
  end

  C.set_source(ctx, color)

  C.move_to(ctx, x, candle.tail_top)
  C.line_to(ctx, x, candle.body_top)

  body_width = 0.8
  body_left = x - (body_width / 2)
  body_height = candle.body_top - candle.body_bottom
  C.rectangle(ctx,
                  body_left, candle.body_bottom,
                  body_width, body_height)

  C.move_to(ctx, x, candle.body_bottom)
  C.line_to(ctx, x, candle.tail_bottom)

  C.set_line_width(ctx,1)
  C.stroke(ctx)
end

function draw_the_candles(ctx, candles)
  for (candle_ix, candle) in enumerate(candles)
    x = candle_ix
    draw_candle(ctx, x, candle)
  end
end

function window_x_to_bucket_ix(m, x) :: Integer
  x = convert(Float64, x)
  user_x, _user_y = Tr.undo(m, x, 0)
  round(user_x)
end

function get_ixs_in_horizontal_view(m, window_width)
  window_width = round(window_width)
  pixel_on_the_left, pixel_on_the_right = [1, window_width - 1]
  first_ix, last_ix = map(pixel -> window_x_to_bucket_ix(m, pixel),
                          [pixel_on_the_left, pixel_on_the_right])
  [first_ix, last_ix]
end

function calc_ohlc_mean(bucket :: Bucket) :: Float64
  mean([bucket.open, bucket.high, bucket.low, bucket.close])
end

function crop_ixs_to_length(len, first_ix, last_ix)
  # both ixs are at least 1
  first_ix = 1 > first_ix ? 1 : first_ix
  last_ix = 1 > last_ix ? 1 : last_ix

  # both ixs are not beyond len
  first_ix = len < first_ix ? len : first_ix
  last_ix = len < last_ix ? len : last_ix

  [first_ix last_ix]
end

function take_if_exists(data :: Vector{T}, first_ix, last_ix) where {T}
  if isempty(data)
    Vector{T}()
  else
    len = length(data)
    first_ix, last_ix = crop_ixs_to_length(len, first_ix, last_ix)
    @view data[first_ix : last_ix]
  end
end

function get_avg_price_in_view(m :: Tr.TransformMatrix,
                               buckets :: Vector{Bucket},
                               window_width :: Float64
                              ) :: Float64
  first_ix, last_ix = get_ixs_in_horizontal_view(m, window_width)
  buckets_in_view = take_if_exists(buckets, first_ix, last_ix)
  # mean(preproc_f, data)
  mean(calc_ohlc_mean, buckets_in_view)
end

function to_cairo_matrix(m :: Tr.TransformMatrix)
  C.CairoMatrix(m[1,1], m[2,1], m[1,2], m[2,2], m[1,3], m[2,3])
end


function calc_transf_matrix(canvas_size,
                            buckets :: Vector{Bucket},
                            zoom :: XY,
                            pan :: XY)
  window_width, window_height = canvas_size
  zoom_x, zoom_y = zoom
  window_width_across_from_last_candle = 
    length(buckets) - (window_width / zoom_x)
  scroll_x, scroll_y = [0.,0.]
  scroll_x = scroll_x + window_width_across_from_last_candle
  right_margin_in_candles = 3
  scroll_x = scroll_x + right_margin_in_candles
  scroll_x = scroll_x - pan[1]
  # horizontalus shiftas turetu vykti cia.
  # bet tik ne vertikalus, nes jis priklauso nuo
  # avg_price_in_view, o tai priklauso nuo scroll-x.
  m = Tr.identity_transform
  m = Tr.translate(m, scroll_x * -1, 0)
  # horizontal zoom
  m = Tr.scale(m, zoom_x, 1)
  avg_price_in_view = begin
    if isempty(buckets)
      0
    else
      get_avg_price_in_view(m, buckets, window_width)
    end
  end
  scroll_y = scroll_y + (-1 * avg_price_in_view)
  scroll_y = scroll_y + pan[2]
  # vertical origin = data
  m = Tr.translate(m, 0, scroll_y)
  # vertical zoom
  m = Tr.scale(m, 1, zoom_y)
  # vertical window center
  m = Tr.translate(m, 0, -0.5 * window_height)
  # vertical flip
  m = Tr.scale(m, 1, -1)
  m
end

function calc_zoom(atr_at_focus :: Float64, zoom_y_factor :: Float64
                  ) :: XY
  zoom_x = 9
  zoom_y = zoom_y_factor / atr_at_focus
  XY(zoom_x, zoom_y)
end

function size_signal(canvas) :: R.Signal{XY}
  function rectangle_dimensions_to_xy(rect)
    XY(rect.width, rect.height)
  end
  gtk_to_signal(canvas,
                "size-allocate",
                rectangle_dimensions_to_xy,
                zero(XY))
end

function run()

  slider_win = G.Window("Sliders", 300, 60) |> (slider_box = G.Box(:v));
  main_win = G.Window("Candles", 700, 500)

  G.set_gtk_property!(slider_win, :transient_for, main_win)
  G.set_gtk_property!(slider_win, :destroy_with_parent, true)

  canvas = G.@GtkCanvas()
  push!(main_win, canvas)

  canvas_size = size_signal(canvas)

  G.showall(main_win)
  G.showall(slider_win)
  G.destroy(slider_win)

  buckets = R.Signal(Vector{Bucket}())
  candles = map(buckets -> map(bucket2candle, buckets), buckets)

  focus_ix = map(length, buckets)
  atr_at_focus = map(focus_ix, buckets) do ix, buckets
    period = 20
    atr = Tech.calc_atr_at_ix(buckets, ix, period)
    @debug "atr_at_focus" atr
    atr
  end

  zoom_y_factor = R.Signal(50 / 3)
  zoom = map(calc_zoom, atr_at_focus, zoom_y_factor)

  zoom_y_factor_slider = GR.slider(5 : 0.5 : 40, signal=zoom_y_factor)
  push!(slider_box, zoom_y_factor_slider)

  @async begin
    _buckets =
    try
      Bitmex.get_buckets("XBTUSD",5,500)
    catch e
      @error "bitmex get failed" e
      throw(e)
    end
    push!(buckets, _buckets)
  end

  foreach(buckets) do buckets
    @debug "buckets length" length(buckets)
  end

  function set_color(ctx, color)
    C.set_source_rgb(ctx, color.r, color.g, color.b)
  end

  device_pan = Interaction.pan_signal(canvas)

  pan = map(zoom, device_pan) do zoom, device_pan
    x = device_pan[1] / zoom[1]
    y = (device_pan[2] / zoom[2]) * -1
    XY(x,y)
  end

  m = map(canvas_size, buckets, zoom, pan) do canvas_size, buckets, zoom, pan
    m = calc_transf_matrix(canvas_size, buckets, zoom, pan)
    m
  end

  function set_cairo_matrix(ctx, m :: Tr.TransformMatrix)
    cairo_m = to_cairo_matrix(m)
    C.set_matrix(ctx, cairo_m)
  end

 #canvas_middle_point_in_user = map(canvas_size, m) do canvas_size, m
 #  middle_point_device = canvas_size / 2
 #  Tr.undo(m, middle_point_device)
 #end

 #function draw_the_pan(ctx, canvas_middle_point_in_user, pan)
 #    C.set_source(ctx, colorant"green")
 #    origin = canvas_middle_point_in_user
 #    C.move_to(ctx, origin[1], origin[2])
 #    panned = origin + pan
 #    @debug "panned viz" origin pan diff panned
 #    C.line_to(ctx, panned[1], panned[2])
 #end

  @debug "draw_callback insantiation"
  foreach(candles, m
         ) do candles, m
    G.@guarded G.draw(canvas) do canvas
      ctx = G.getgc(canvas)
      set_color(ctx, colorant"black")
      C.paint(ctx)
      C.set_antialias(ctx, C.ANTIALIAS_NONE)
      set_cairo_matrix(ctx, m)
      #draw_the_pan(ctx, canvas_middle_point_in_user, pan)
      draw_the_candles(ctx, candles)
    end
  end

  main_win_closed = Condition()
  G.signal_connect(main_win, :destroy) do widget
    @debug "main_win destroyed"
    notify(main_win_closed)
  end
  @debug "waiting for main_win_closed"
  wait(main_win_closed)
  @info "program stop"

end

end
